src/libostree/ostree-soft-reboot.c \
src/libostree/ostree-impl-system-generator.c \
src/libostree/ostree-bootconfig-parser.c \
+ src/libostree/ostree-bootconfig-parser-private.h \
src/libostree/ostree-deployment.c \
src/libostree/ostree-bootloader.h \
src/libostree/ostree-bootloader.c \
tests/test-osupdate-dtb.sh \
tests/test-admin-instutil-set-kargs.sh \
tests/test-admin-upgrade-not-backwards.sh \
+ tests/test-admin-boot-counting-tries.sh \
tests/test-admin-pull-deploy-commit.sh \
tests/test-admin-pull-deploy-split.sh \
tests/test-admin-locking.sh \
_installed_or_uninstalled_test_programs = tests/test-varint tests/test-ot-unix-utils tests/test-bsdiff tests/test-otcore tests/test-mutable-tree \
tests/test-keyfile-utils tests/test-ot-opt-utils tests/test-ot-tool-util \
- tests/test-checksum tests/test-lzma tests/test-rollsum \
+ tests/test-checksum tests/test-lzma tests/test-rollsum tests/test-bootconfig-parser-internals \
tests/test-basic-c tests/test-sysroot-c tests/test-pull-c tests/test-repo tests/test-include-ostree-h tests/test-kargs \
tests/test-rfc2616-dates tests/test-pem
tests_test_kargs_CFLAGS = $(TESTS_CFLAGS)
tests_test_kargs_LDADD = $(TESTS_LDADD)
+tests_test_bootconfig_parser_internals_SOURCES = tests/test-bootconfig-parser-internals.c
+tests_test_bootconfig_parser_internals_CFLAGS = $(TESTS_CFLAGS)
+tests_test_bootconfig_parser_internals_LDADD = $(TESTS_LDADD)
+
tests_test_repo_finder_config_SOURCES = tests/test-repo-finder-config.c
tests_test_repo_finder_config_CFLAGS = $(TESTS_CFLAGS)
tests_test_repo_finder_config_LDADD = $(TESTS_LDADD)
ostree_bootconfig_parser_get
ostree_bootconfig_parser_set_overlay_initrds
ostree_bootconfig_parser_get_overlay_initrds
+ostree_bootconfig_parser_get_tries_left
+ostree_bootconfig_parser_get_tries_done
<SUBSECTION Standard>
OSTREE_BOOTCONFIG_PARSER
OSTREE_IS_BOOTCONFIG_PARSER
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><varname>boot-counting-tries</varname></term>
+ <listitem><para>Integer value controlling the number of maximum boot attempts. The boot
+ counting data is stored in the name of the boot loader entry. A boot loader entry file name
+ may contain a plus (+) followed by a number. This may optionally be followed by
+ a minus (-) followed by a second number. The dot (.) and file name suffix (conf or efi) must
+ immediately follow. More details in the
+ <ulink url="https://uapi-group.org/specifications/specs/boot_loader_specification/#boot-counting">
+ The Boot Loader Specification</ulink>
+ </para></listitem>
+ </varlistentry>
+
<varlistentry>
<term><varname>bls-append-except-default</varname></term>
<listitem><para>A semicolon separated string list of key-value pairs. For example:
ostree_sysroot_deployment_can_soft_reboot;
ostree_sysroot_deployment_set_soft_reboot;
ostree_sysroot_clear_soft_reboot;
+ ostree_bootconfig_parser_get_tries_left;
+ ostree_bootconfig_parser_get_tries_done;
} LIBOSTREE_2025.2;
--- /dev/null
+/* SPDX-License-Identifier: LGPL-2.0+ */
+
+#pragma once
+
+#include "ostree-bootconfig-parser.h"
+
+G_BEGIN_DECLS
+
+const char *_ostree_bootconfig_parser_filename (OstreeBootconfigParser *self);
+
+G_END_DECLS
#include "config.h"
-#include "ostree-bootconfig-parser.h"
+#include "ostree-bootconfig-parser-private.h"
#include "otutil.h"
struct _OstreeBootconfigParser
{
GObject parent_instance;
- gboolean parsed;
+ char *filename;
const char *separators;
+ guint64 tries_left;
+ guint64 tries_done;
+
GHashTable *options;
/* Additional initrds; the primary initrd is in options. */
GLNX_HASH_TABLE_FOREACH_KV (self->options, const char *, k, const char *, v)
g_hash_table_replace (parser->options, g_strdup (k), g_strdup (v));
+ parser->filename = g_strdup (self->filename);
parser->overlay_initrds = g_strdupv (self->overlay_initrds);
return parser;
}
+/*
+ * Parses a suffix of two counters in the form "+LEFT-DONE" from the end of the
+ * filename (excluding file extension).
+ */
+static void
+parse_bootloader_tries (const char *filename, guint64 *out_left, guint64 *out_done)
+{
+ *out_left = 0;
+ *out_done = 0;
+
+ const char *counter = strrchr (filename, '+');
+ if (!counter)
+ return;
+ counter += 1;
+
+ guint64 tries_left = 0;
+ guint64 tries_done = 0;
+
+ // Negative numbers are invalid
+ if (*counter == '-')
+ return;
+
+ {
+ char *endp = NULL;
+ tries_left = g_ascii_strtoull (counter, &endp, 10);
+ if (endp == counter || (tries_left == G_MAXUINT64 && errno == ERANGE))
+ return;
+ counter = endp;
+ }
+
+ /* Parse done counter only if present */
+ if (*counter == '-')
+ {
+ counter += 1;
+ char *endp = NULL;
+ tries_done = g_ascii_strtoull (counter, &endp, 10);
+ if (endp == counter || (tries_done == G_MAXUINT64 && errno == ERANGE))
+ return;
+ }
+
+ *out_left = tries_left;
+ *out_done = tries_done;
+}
+
+/**
+ * ostree_bootconfig_parser_get_tries_left:
+ * @self: Parser
+ *
+ * Returns: Amount of boot tries left
+ *
+ * Since: 2025.2
+ */
+guint64
+ostree_bootconfig_parser_get_tries_left (OstreeBootconfigParser *self)
+{
+ return self->tries_left;
+}
+
+/**
+ * ostree_bootconfig_parser_get_tries_done:
+ * @self: Parser
+ *
+ * Returns: Amount of boot tries
+ */
+guint64
+ostree_bootconfig_parser_get_tries_done (OstreeBootconfigParser *self)
+{
+ return self->tries_done;
+}
+
+const char *
+_ostree_bootconfig_parser_filename (OstreeBootconfigParser *self)
+{
+ return self->filename;
+}
+
/**
* ostree_bootconfig_parser_parse_at:
* @self: Parser
ostree_bootconfig_parser_parse_at (OstreeBootconfigParser *self, int dfd, const char *path,
GCancellable *cancellable, GError **error)
{
- g_assert (!self->parsed);
+ g_assert (!self->filename);
g_autofree char *contents = glnx_file_get_contents_utf8_at (dfd, path, NULL, cancellable, error);
if (!contents)
self->overlay_initrds = (char **)g_ptr_array_free (g_steal_pointer (&overlay_initrds), FALSE);
}
- self->parsed = TRUE;
+ const char *basename = glnx_basename (path);
+ parse_bootloader_tries (basename, &self->tries_left, &self->tries_done);
+ self->filename = g_strdup (basename);
return TRUE;
}
{
OstreeBootconfigParser *self = OSTREE_BOOTCONFIG_PARSER (object);
+ g_free (self->filename);
g_strfreev (self->overlay_initrds);
g_hash_table_unref (self->options);
_OSTREE_PUBLIC
char **ostree_bootconfig_parser_get_overlay_initrds (OstreeBootconfigParser *self);
+_OSTREE_PUBLIC
+guint64 ostree_bootconfig_parser_get_tries_left (OstreeBootconfigParser *self);
+
+_OSTREE_PUBLIC
+guint64 ostree_bootconfig_parser_get_tries_done (OstreeBootconfigParser *self);
+
G_END_DECLS
GHashTable
*bls_append_values; /* Parsed key-values from bls-append-except-default key in config. */
gboolean enable_bootprefix; /* If true, prepend bootloader entries with /boot */
+ guint boot_counting;
OstreeRepo *parent_repo;
};
static gboolean
reload_sysroot_config (OstreeRepo *self, GCancellable *cancellable, GError **error)
{
+ g_autofree char *boot_counting_str = NULL;
+
+ if (!ot_keyfile_get_value_with_default_group_optional (
+ self->config, "sysroot", "boot-counting-tries", "0", &boot_counting_str, error))
+ return FALSE;
+ guint64 v;
+ if (!g_ascii_string_to_unsigned (boot_counting_str, 10, 0, 5, &v, error))
+ return glnx_prefix_error (error, "Parsing sysroot.boot-counting-tries");
+ self->boot_counting = (guint)v;
+
g_autofree char *bootloader = NULL;
if (!ot_keyfile_get_value_with_default_group_optional (self->config, "sysroot", "bootloader",
#include <gio/gunixinputstream.h>
#include <gio/gunixoutputstream.h>
#include <glib-unix.h>
+#include <inttypes.h>
#include <linux/kexec.h>
#include <stdbool.h>
#include <stdint.h>
#endif
#include "libglnx.h"
+#include "ostree-bootconfig-parser-private.h"
#include "ostree-core-private.h"
#include "ostree-deployment-private.h"
#include "ostree-linuxfsutil.h"
bootloader_entry_filename (OstreeSysroot *sysroot, guint n_deployments,
OstreeDeployment *deployment)
{
+ g_autofree char *bootconf_name = NULL;
guint index = n_deployments - ostree_deployment_get_index (deployment);
// Allow opt-out to dropping the stateroot in case of compatibility issues.
// As of 2024.5, we have a new naming scheme because grub2 parses the *filename* and ignores
if (use_old_naming)
{
const char *stateroot = ostree_deployment_get_osname (deployment);
- return g_strdup_printf ("ostree-%d-%s.conf", index, stateroot);
+ bootconf_name = g_strdup_printf ("ostree-%d-%s", index, stateroot);
}
else
{
- return g_strdup_printf ("ostree-%d.conf", index);
+ bootconf_name = g_strdup_printf ("ostree-%d", index);
}
+
+ if (!sysroot->repo->boot_counting)
+ return g_strdup_printf ("%s.conf", bootconf_name);
+
+ guint max_tries = sysroot->repo->boot_counting;
+ OstreeBootconfigParser *bootconfig = ostree_deployment_get_bootconfig (deployment);
+
+ if (!_ostree_bootconfig_parser_filename (bootconfig))
+ return g_strdup_printf ("%s+%u.conf", bootconf_name, max_tries);
+ else if (!ostree_bootconfig_parser_get_tries_left (bootconfig)
+ && !ostree_bootconfig_parser_get_tries_done (bootconfig))
+ return g_strdup_printf ("%s.conf", bootconf_name);
+ else
+ return g_strdup_printf ("%s+%" PRIu64 "-%" PRIu64 ".conf", bootconf_name,
+ ostree_bootconfig_parser_get_tries_left (bootconfig),
+ ostree_bootconfig_parser_get_tries_done (bootconfig));
}
/* Given @deployment, prepare it to be booted; basically copying its
const char *bootcsum = ostree_deployment_get_bootcsum (deployment);
g_autofree char *bootcsumdir = g_strdup_printf ("ostree/%s-%s", osname, bootcsum);
g_autofree char *bootconfdir = g_strdup_printf ("loader.%d/entries", new_bootversion);
- g_autofree char *bootconf_name = bootloader_entry_filename (sysroot, n_deployments, deployment);
if (!glnx_shutil_mkdir_p_at (sysroot->boot_fd, bootcsumdir, 0775, cancellable, error))
return FALSE;
if (!glnx_opendirat (sysroot->boot_fd, bootconfdir, TRUE, &bootconf_dfd, error))
return FALSE;
+ g_autofree char *bootconf_filename
+ = bootloader_entry_filename (sysroot, n_deployments, deployment);
+
if (!ostree_bootconfig_parser_write_at (ostree_deployment_get_bootconfig (deployment),
- bootconf_dfd, bootconf_name, cancellable, error))
+ bootconf_dfd, bootconf_filename, cancellable, error))
return FALSE;
return TRUE;
OstreeBootconfigParser *new_bootconfig = ostree_deployment_get_bootconfig (deployment);
ostree_bootconfig_parser_set (new_bootconfig, "options", kargs_str);
- g_autofree char *bootconf_name
+ g_autofree char *bootconf_filename
= bootloader_entry_filename (self, self->deployments->len, deployment);
g_autofree char *bootconfdir = g_strdup_printf ("loader.%d/entries", self->bootversion);
if (!glnx_opendirat (self->boot_fd, bootconfdir, TRUE, &bootconf_dfd, error))
return FALSE;
- if (!ostree_bootconfig_parser_write_at (new_bootconfig, bootconf_dfd, bootconf_name,
+ if (!ostree_bootconfig_parser_write_at (new_bootconfig, bootconf_dfd, bootconf_filename,
cancellable, error))
return FALSE;
}
--- /dev/null
+#!/bin/bash
+#
+# SPDX-License-Identifier: LGPL-2.0+
+
+set -euo pipefail
+
+. $(dirname $0)/libtest.sh
+
+setup_os_repository "archive" "syslinux"
+
+${CMD_PREFIX} ostree --repo=sysroot/ostree/repo config set sysroot.boot-counting-tries 3
+v=$(${CMD_PREFIX} ostree --repo=sysroot/ostree/repo config get sysroot.boot-counting-tries)
+assert_streq "$v" 3
+
+tap_ok "init boot counting tries"
+
+${CMD_PREFIX} ostree --repo=sysroot/ostree/repo pull-local --remote=testos testos-repo testos/buildmain/x86_64-runtime
+rev=$(${CMD_PREFIX} ostree --repo=sysroot/ostree/repo rev-parse testos/buildmain/x86_64-runtime)
+export rev
+
+${CMD_PREFIX} ostree admin deploy --karg=quiet --stateroot=testos testos:testos/buildmain/x86_64-runtime
+entry=$(ls sysroot/boot/loader/entries/)
+assert_streq "${entry}" ostree-1+3.conf
+
+tap_ok "deploy with boot counting"
+
+tap_end
--- /dev/null
+/*
+ * SPDX-License-Identifier: LGPL-2.0+
+ */
+
+#include "config.h"
+#define _OSTREE_PUBLIC
+#include "../src/libostree/ostree-bootconfig-parser.c"
+
+static void
+test_parse_tries_valid (void)
+{
+ guint64 left, done;
+ parse_bootloader_tries ("foo", &left, &done);
+ g_assert_cmpuint (left, ==, 0);
+ g_assert_cmpuint (done, ==, 0);
+
+ parse_bootloader_tries ("foo+1", &left, &done);
+ g_assert_cmpuint (left, ==, 1);
+ g_assert_cmpuint (done, ==, 0);
+
+ parse_bootloader_tries ("foo+1-2", &left, &done);
+ g_assert_cmpuint (left, ==, 1);
+ g_assert_cmpuint (done, ==, 2);
+
+ parse_bootloader_tries ("foo+1-2.conf", &left, &done);
+ g_assert_cmpuint (left, ==, 1);
+ g_assert_cmpuint (done, ==, 2);
+}
+
+static void
+test_parse_tries_invalid (void)
+{
+ guint64 left, done;
+
+ parse_bootloader_tries ("foo+1-", &left, &done);
+ g_assert_cmpuint (left, ==, 0);
+ g_assert_cmpuint (done, ==, 0);
+
+ parse_bootloader_tries ("foo+-1", &left, &done);
+ g_assert_cmpuint (left, ==, 0);
+ g_assert_cmpuint (done, ==, 0);
+
+ parse_bootloader_tries ("foo+1-a", &left, &done);
+ g_assert_cmpuint (left, ==, 0);
+ g_assert_cmpuint (done, ==, 0);
+
+ parse_bootloader_tries ("foo+a-1", &left, &done);
+ g_assert_cmpuint (left, ==, 0);
+ g_assert_cmpuint (done, ==, 0);
+}
+
+int
+main (int argc, char *argv[])
+{
+ g_test_init (&argc, &argv, NULL);
+
+ g_test_add_func ("/bootconfig-parser/tries/valid", test_parse_tries_valid);
+ g_test_add_func ("/bootconfig-parser/tries/invalid", test_parse_tries_invalid);
+ return g_test_run ();
+}